##########################################
## Copyright 2012-2013 Ceruti Francesco & contributors
##
## This file is part of LiSP (Linux Show Player).
##########################################

from copy import deepcopy
import math
import os
from threading import Thread, Timer
from time import clock
import time

from PyQt4.QtCore import pyqtSignal
import gi
from gi.repository import Gst
from lisp.core.media import Media
from lisp.utils import audio_utils, configuration as cfg


gi.require_version('Gst', '1.0')


class GstTrack(Media):

    # GstTrack's configuration parameters
    Media.MEDIA_DEFAULT_CONF.update({'volume': {'volume': 1, 'mute': False},
                             'equalizer': {'band0': 0,
                                           'band1': 0,
                                           'band2': 0,
                                           'band3': 0,
                                           'band4': 0,
                                           'band5': 0,
                                           'band6': 0,
                                           'band7': 0,
                                           'band8': 0,
                                           'band9': 0},
                             'pitch': {'tempo': 1,
                                       'pitch': 1},
                             'audiodynamic': {'mode': 'compressor',
                                              'characteristics': 'hard-knee',
                                              'ratio': 1,
                                              'threshold': 1},
                             'fadeIn': {'time': 0,
                                        'exp': 1.5},
                             'fadeOut': {'time': 0,
                                         'exp': 3},
                             'duration': -1,
                             'mtime': -1,
                             'loop': False,
                             'wait': 0,
                             'endFadeOut': False,
                             'pipeline': cfg.config['GStreamer']['DefaultPipeline']})

    onLevel = pyqtSignal(list, list)

    def __init__(self, conf=None, uuid=None):
        super(GstTrack, self).__init__(uuid)

        self.gstState = Gst.State.NULL
        self.dPipe = {}
        self.gstPipe = None

        self.conf.update(deepcopy(Media.MEDIA_DEFAULT_CONF))
        self.createPipeline()
        self.updateConfiguration(conf)

    def createPipeline(self):
        if(self.state != GstTrack.NONE):
            self.interrupt(False)

        self.gstPipe = Gst.parse_launch(self.conf['pipeline'])

        # Put the various elements of pipeline in a dict
        for index in range(self.gstPipe.get_children_count()):
            elem = self.gstPipe.get_child_by_index(index)
            self.dPipe[elem.get_property('name')] = elem

        # Get the pipeline bus and link it to __on_message function
        bus = self.gstPipe.get_bus()
        bus.add_signal_watch()
        bus.connect("message", self.__on_message)

    def updateConfiguration(self, conf, emit=True, duration=True):
        if(conf is not None):
            if('pipeline' in conf):
                if(self.conf['pipeline'] != conf['pipeline']):
                    self.conf['pipeline'] = conf['pipeline']
                    self.createPipeline()

            self.conf.update(conf)

            temp_location = None
            if('location' in conf and (self.state == Media.PLAYING or self.state == Media.PAUSED)):
                temp_location = conf.pop('location')

            # Set elements properties, using conf, not self.conf
            for el in self.dPipe:
                if(el in conf):
                    if(isinstance(conf[el], dict)):
                        for pr in conf[el]:
                            self.dPipe[el].set_property(pr, conf[el][pr])
                    else:
                        self.dPipe[el].set_property(el, conf[el])

            if(temp_location is not None):
                self.conf['location'] = temp_location

            if(duration and os.path.exists(self.conf['location'])):
                if(self.conf['mtime'] != os.path.getmtime(self.conf['location']) or self.conf['duration'] == -1):
                    self.conf['mtime'] = os.path.getmtime(self.conf['location'])
                    fext = audio_utils.extension(self.conf['location'])
                    if(fext == '.wav' or fext == '.wave'):
                        self.__duration()
                    else:
                        thread = Thread(target=self.__duration)
                        thread.setDaemon(True)
                        thread.start()
                else:
                    self.onDuration.emit(self, self.conf['duration'])
            elif(not os.path.exists(self.conf['location'])):
                raise Exception('Audio file not exists: ' + self.conf['location'])

            if(self.state == GstTrack.NONE):
                self.state = GstTrack.STOPPED

            if(emit):
                self.onConfigurationChanged.emit(self)

    def play(self, emit=True):
        # If state is not equals to STOPPED or PAUSED the track cannot be played
        if(self.state == GstTrack.STOPPED or self.state == GstTrack.PAUSED):
            self.state = GstTrack.PLAYING
            # Reset the volume property
            self.dPipe['volume'].set_property('volume', self.conf['volume']['volume'])

            if(emit):
                self.onPlay.emit(self)
            if(self.conf['wait'] > 0):
                Timer(self.conf['wait'], self.__play_process).start()
            else:
                self.__play_process()

    def stop(self, emit=True):
        # If state is not equals to PLAYING or PAUSED the track cannot be stopped
        if(self.state == GstTrack.PLAYING or self.state == GstTrack.PAUSED):
            self.state = GstTrack.STOPPED

            self.stop_thread = Thread(target=self.__stop_process, args=(emit,))
            self.stop_thread.setDaemon(True)

            if(emit):
                self.onStop.emit(self)
            self.stop_thread.start()

    def pause(self, emit=True):
        # If state is not equals to PLAYING the track cannot be paused
        if(self.state == GstTrack.PLAYING):
            self.state = GstTrack.PAUSED

            self.stop_thread = Thread(target=self.__pause_process, args=(emit,))
            self.stop_thread.setDaemon(True)

            if(emit):
                self.onPause.emit(self)
            self.stop_thread.start()

    def seek(self, seek_time, emit=True):
        # If state is NONE the track cannot be sought
        if(self.state != GstTrack.NONE):
            if(seek_time < self.conf['duration']):
                if(self.state == GstTrack.STOPPED or self.state == GstTrack.PAUSED):
                    self.play()
                else:
                    self.gstPipe.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, seek_time * Gst.MSECOND)

        if(emit):
            self.onSeek.emit(self, seek_time)

    def currentTime(self):
        return int(self.gstPipe.query_position(Gst.Format.TIME)[1] / Gst.MSECOND)

    def interrupt(self, reconf=True):
        self.gstPipe.set_state(Gst.State.NULL)
        self.state = GstTrack.NONE
        if(reconf):
            self.createPipeline()
            self.updateConfiguration(self.conf, emit=False, duration=False)

    def __play_process(self):
        # If fadeIn is > 0 start the track with the fadeIn thread
        if(self.conf['fadeIn']['time'] > 0):
            self.dPipe['volume'].set_property('volume', 0)
            self.fadeIn_thread = Thread(target=self.__fadein)
            self.fadeIn_thread.setDaemon(True)
            self.gstPipe.set_state(Gst.State.PLAYING)
            self.fadeIn_thread.start()
        else:
            self.gstPipe.set_state(Gst.State.PLAYING)

        # If endFadeOut is enabled and fadeOut is > 0 start endFadeOut thread
        if(self.conf['endFadeOut'] and self.conf['fadeOut']['time'] > 0):
            self.endFadeOut_thread = Thread(target=self.__endfadeout)
            self.endFadeOut_thread.setDaemon(True)
            self.endFadeOut_thread.start()

    def __pause_process(self, emit):
        pause = True
        if(self.conf['fadeOut']['time'] > 0):
            pause = self.__fadeout()
        if(pause):
            self.gstPipe.set_state(Gst.State.PAUSED)
            if(emit):
                self.onPaused.emit(self)

    def __stop_process(self, emit):
        stop = True
        if(self.conf['fadeOut']['time'] > 0):
            stop = self.__fadeout()
        if(stop):
            self.interrupt()
            if(emit):
                self.onStopped.emit(self)

    def __on_finish(self):
        self.interrupt()

        if(self.conf['loop'] == True):
            self.play()
        else:
            self.state = GstTrack.STOPPED
            self.onEndOfStream.emit(self)

    def __endfadeout(self):
        while(self.state == GstTrack.PLAYING or self.gstState == Gst.State.PLAYING):
            # If fadeOut (enabled) is > 0 and >= of remaining time start fadeOut thread
            if(self.conf['fadeOut']['time'] >= self.conf['duration'] / 100 - self.currentTime() / 100):
                self.state = GstTrack.STOPPED
                self.__fadeout()
            time.sleep(0.1)

    def __fadein(self):
        self.onFadeIn.emit(True)
        sec = 0
        while(sec < self.conf['fadeIn']['time']):
            if(self.state != GstTrack.PLAYING):
                return False
            self.dPipe['volume'].set_property('volume', (math.pow(sec, self.conf['fadeIn']['exp']) / math.pow(self.conf['fadeIn']['time'], self.conf['fadeIn']['exp'])) * self.conf['volume']['volume'])
            sec += self.conf['fadeIn']['time'] / 1000
            time.sleep(self.conf['fadeIn']['time'] / 10000)
        self.onFadeIn.emit(False)
        return True

    def __fadeout(self):
        self.onFadeOut.emit(True)
        sec = self.conf['fadeOut']['time']
        # If fading out while still in fade-in it would differ, audibly, from self.conf['volume']['volume']
        currentVolume = self.dPipe['volume'].get_property('volume')
        while(sec >= 0):
            if(self.state == GstTrack.PLAYING or self.state == GstTrack.NONE):
                return False
            self.dPipe['volume'].set_property('volume', (math.pow(sec, self.conf['fadeOut']['exp']) / math.pow(self.conf['fadeOut']['time'], self.conf['fadeOut']['exp'])) * currentVolume)
            sec -= self.conf['fadeOut']['time'] / 1000
            time.sleep(self.conf['fadeOut']['time'] / 10000)
        self.onFadeOut.emit(False)
        return True

    # Grab and process the messages on the pipeline bus
    def __on_message(self, bus, message):
        if(message.type == Gst.MessageType.ERROR):
            self.interrupt()
            err, debug = message.parse_error()
            print(str(err) + '\n' + str(debug))
            self.onError.emit(self, str(err) + '\n' + str(debug))
        elif(message.type == Gst.MessageType.EOS):
            self.__on_finish()
        elif(message.type == Gst.MessageType.STATE_CHANGED):
            self.gstState = message.parse_state_changed()[1]
        elif(message.type == Gst.MessageType.ELEMENT):
            struct = message.get_structure()
            if(struct is not None):
                if(struct.has_name('level')):
                    self.onLevel.emit(struct.get_value('peak'), struct.get_value('decay'))

    def __duration(self):
        self.conf['duration'] = int(audio_utils.getDuration(self.conf['location']))
        if(self.conf['duration'] == -1):
            raise Exception('Audio file not readable: ' + self.conf['location'])

        self.onDuration.emit(self, self.conf['duration'])
